<?php

/**
 * Class ReedSolomonEncoder
 *
 * @created      07.01.2021
 *
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2021 smiley
 * @license      MIT
 */
declare(strict_types=1);

namespace zxf\QrCode\Data;

use zxf\QrCode\Common\BitBuffer;
use zxf\QrCode\Common\EccLevel;
use zxf\QrCode\Common\GenericGFPoly;
use zxf\QrCode\Common\GF256;
use zxf\QrCode\Common\Version;

use function array_fill;
use function array_merge;
use function count;
use function max;

/**
 * Reed-Solomon encoding - ISO/IEC 18004:2000 Section 8.5 ff
 *
 * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
 */
final class ReedSolomonEncoder
{
    private Version $version;

    private EccLevel $eccLevel;

    /** @var int[] */
    private array $interleavedData;

    private int $interleavedDataIndex;

    /**
     * ReedSolomonDecoder constructor
     */
    public function __construct(Version $version, EccLevel $eccLevel)
    {
        $this->version = $version;
        $this->eccLevel = $eccLevel;
    }

    /**
     * ECC encoding and interleaving
     *
     * @return int[]
     *
     * @throws \zxf\QrCode\QRCodeException
     */
    public function interleaveEcBytes(BitBuffer $bitBuffer): array
    {
        [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel);

        $rsBlocks = array_fill(0, $l1, [($numEccCodewords + $b1), $b1]);

        if ($l2 > 0) {
            $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [($numEccCodewords + $b2), $b2]));
        }

        $bitBufferData = $bitBuffer->getBuffer();
        $dataBytes = [];
        $ecBytes = [];
        $maxDataBytes = 0;
        $maxEcBytes = 0;
        $dataByteOffset = 0;

        foreach ($rsBlocks as $key => [$rsBlockTotal, $dataByteCount]) {
            $dataBytes[$key] = [];

            for ($i = 0; $i < $dataByteCount; $i++) {
                $dataBytes[$key][$i] = ($bitBufferData[($i + $dataByteOffset)] & 0xFF);
            }

            $ecByteCount = ($rsBlockTotal - $dataByteCount);
            $ecBytes[$key] = $this->encode($dataBytes[$key], $ecByteCount);
            $maxDataBytes = max($maxDataBytes, $dataByteCount);
            $maxEcBytes = max($maxEcBytes, $ecByteCount);
            $dataByteOffset += $dataByteCount;
        }

        $this->interleavedData = array_fill(0, $this->version->getTotalCodewords(), 0);
        $this->interleavedDataIndex = 0;
        $numRsBlocks = ($l1 + $l2);

        $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks);
        $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks);

        return $this->interleavedData;
    }

    /**
     * @param  int[]  $dataBytes
     * @return int[]
     */
    private function encode(array $dataBytes, int $ecByteCount): array
    {
        $rsPoly = new GenericGFPoly([1]);

        for ($i = 0; $i < $ecByteCount; $i++) {
            $rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)]));
        }

        $rsPolyDegree = $rsPoly->getDegree();

        $modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree))
            ->mod($rsPoly)
            ->getCoefficients();

        $ecBytes = array_fill(0, $rsPolyDegree, 0);
        $count = (count($modCoefficients) - $rsPolyDegree);

        foreach ($ecBytes as $i => &$val) {
            $modIndex = ($i + $count);
            $val = 0;

            if ($modIndex >= 0) {
                $val = $modCoefficients[$modIndex];
            }
        }

        return $ecBytes;
    }

    /**
     * @param  int[][]  $byteArray
     */
    private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks): void
    {
        for ($x = 0; $x < $maxBytes; $x++) {
            for ($y = 0; $y < $numRsBlocks; $y++) {
                if ($x < count($byteArray[$y])) {
                    $this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x];
                }
            }
        }
    }
}
